﻿#region License

// PentaBiz - Sustainable Software Development Framework Copyright (C) 2013 Zoltán Csizmazia
// 
// This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

#endregion

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace PentaBiz.Interop
{
    /// <summary>
    ///     A wrapper around AllocConsole, with some nice eventing to handle
    /// </summary>
    /// <remarks>
    ///     Original from: http://huddledmasses.org/nativeconsolecs-run-console-apps-and-get-their-output/
    /// </remarks>
    public class NativeConsole : IDisposable
    {
        #region Nested type: NativeMethods

        /// <summary>
        ///     The API/Interop/PInvoke methods for the NativeConsole
        /// </summary>
        internal unsafe class NativeMethods
        {
            #region  Fields (5)

            public const UInt32 DUPLICATE_SAME_ACCESS = 0x00000002;
            public const int LWA_ALPHA = 0x2;
            public const int LWA_COLORKEY = 0x1;
            public const int WS_EX_LAYERED = 0x80000;
            public const int WS_EX_TRANSPARENT = 0x00000020;

            #endregion

            #region  Enums (3)

            #region GwlIndex enum

            public enum GwlIndex
            {
                Id = (-12),
                Style = (-16),
                ExStyle = (-20)
            }

            #endregion

            #region ShowState enum

            public enum ShowState
            {
                SW_HIDE = 0
                // and lots of others
            }

            #endregion

            #region StdHandle enum

            public enum StdHandle
            {
                /// <summary>
                ///     The standard input device
                /// </summary>
                INPUT_HANDLE = -10, //(DWORD)-10    The standard input device.
                /// <summary>
                ///     The standard output device.
                /// </summary>
                OUTPUT_HANDLE = -11, //(DWORD)-11   The standard output device.
                /// <summary>
                ///     The standard error device.
                /// </summary>
                ERROR_HANDLE = -12 //(DWORD)-12  The standard error device.
            }

            #endregion

            #endregion

            #region  Methods (13)

            //  Public Methods (13)

            [DllImport("kernel32")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool AllocConsole();

            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool CloseHandle(IntPtr hHandle);

            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe,
                                                 ref SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize);

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle,
                                                      IntPtr hSourceHandle, IntPtr hTargetProcessHandle,
                                                      out IntPtr lpTargetHandle,
                                                      uint dwDesiredAccess, bool bInheritHandle, uint dwOptions);

            [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool FreeConsole();

            [DllImport("kernel32.dll")]
            public static extern IntPtr GetConsoleWindow();

            [DllImport("user32.dll", SetLastError = true)]
            public static extern int GetWindowLong(IntPtr hWnd, GwlIndex nIndex);

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool ReadFile(
                IntPtr hFile, // handle to file
                byte[] lpBuffer, // data buffer
                int nNumberOfBytesToRead, // number of bytes to read
                out int lpNumberOfBytesRead, // number of bytes read
                NativeOverlapped* overlapped // overlapped buffer
                );

            [DllImport("user32.dll")]
            public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, int crKey, uint bAlpha, int dwFlags);

            [DllImport("kernel32.dll")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool SetStdHandle(StdHandle nStdHandle, IntPtr hHandle);

            [DllImport("user32.dll")]
            public static extern int SetWindowLong(IntPtr hWnd, GwlIndex nIndex, int dwNewLong);

            [DllImport("user32.dll")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool ShowWindow(IntPtr hWnd, ShowState nCmdShow);

            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern int WriteFile(IntPtr hFile, byte[] buffer, int numBytesToWrite, out int numBytesWritten,
                                               NativeOverlapped* lpOverlapped);

            #endregion

            #region Nested type: SECURITY_ATTRIBUTES

            [StructLayout(LayoutKind.Sequential)]
            public struct SECURITY_ATTRIBUTES
            {
                public int nLength;
                public IntPtr lpSecurityDescriptor;
                [MarshalAs(UnmanagedType.Bool)] public bool bInheritHandle;
            }

            #endregion
        }

        #endregion

        #region Nested type: OutputEventArgs

        public class OutputEventArgs : EventArgs
        {
            public OutputEventArgs(string text)
            {
                Text = text;
            }

            public string Text { get; private set; }
        }

        #endregion

        #region Delegate and Events (3)

        #region Delegates

        public delegate void OutputDelegate(object sender, OutputEventArgs e);

        #endregion

        // Delegate, I changed this to be compatible with normal wpf/forms events

        // Events (2)
        public event OutputDelegate WriteErrorLine;
        public event OutputDelegate WriteOutputLine;

        #endregion

        #region  Private Fields (14)

        // Track whether Dispose has been called.
        // A nice handle to our console window
        private readonly Thread errorThread;
        private readonly IntPtr handle;
        // And our process
        // Our two threads
        private readonly Thread outputThread;
        private readonly Process process;
        private readonly IntPtr stdErrRead;
        private readonly IntPtr stdErrReadCopy;
        private readonly IntPtr stdErrWrite;
        private readonly IntPtr stdInRead;
        private readonly IntPtr stdInWrite;
        // and the copy handles ...
        private readonly IntPtr stdInWriteCopy;
        private readonly IntPtr stdOutRead;
        private readonly IntPtr stdOutReadCopy;
        private readonly IntPtr stdOutWrite;
        private bool disposed;

        #endregion

        #region  Constructors and Destructors (2)

        /// <summary>
        ///     Initializes a new instance of the <see cref="NativeConsole" /> class.
        /// </summary>
        public NativeConsole()
        {
            // Make ourselves a nice console
            NativeMethods.AllocConsole();
            // hide the window ...
            handle = NativeMethods.GetConsoleWindow();
            NativeMethods.ShowWindow(handle, NativeMethods.ShowState.SW_HIDE);
            //NativeMethods.SetWindowLong(handle, NativeMethods.GwlIndex.ExStyle,
            // (NativeMethods.GetWindowLong(handle, NativeMethods.GwlIndex.ExStyle) |
            // NativeMethods.WS_EX_LAYERED | NativeMethods.WS_EX_TRANSPARENT));
            //NativeMethods.SetLayeredWindowAttributes(handle, 0, 0, NativeMethods.LWA_ALPHA);

            process = Process.GetCurrentProcess();


            NativeMethods.SECURITY_ATTRIBUTES saAttr;

            // Set the bInheritHandle flag so pipe handles are inherited.
            saAttr.nLength = Marshal.SizeOf(typeof (NativeMethods.SECURITY_ATTRIBUTES));
            saAttr.bInheritHandle = true;
            saAttr.lpSecurityDescriptor = IntPtr.Zero;


            // The steps for redirecting STDOUT:
            // * Create anonymous pipe to be STDOUT for us.
            // * Set STDOUT of our process to be WRITE handle to the pipe.
            // * Create a (noninheritable) duplicate of the read handle, and...
            // * Close the inheritable read handle.
            if (!NativeMethods.CreatePipe(out stdOutRead, out stdOutWrite, ref saAttr, 0))
            {
                Trace.TraceError("Couldn't create the STDOUT pipe");
            }
            if (!NativeMethods.SetStdHandle(NativeMethods.StdHandle.OUTPUT_HANDLE, stdOutWrite))
            {
                Trace.TraceError("Couldn't redirect STDOUT!");
            }
            // Create noninheritable read handle and close the inheritable read handle.
            if (!NativeMethods.DuplicateHandle(process.Handle, stdOutRead, process.Handle,
                                               out stdOutReadCopy, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
            {
                Trace.TraceError("Couldn't Duplicate STDOUT Handle");
            }
            NativeMethods.CloseHandle(stdOutRead);

            // For the output handles we need a thread to read them
            outputThread = new Thread(OutputThread);
            outputThread.SetApartmentState(ApartmentState.STA);
            outputThread.Start();

            // The steps for redirecting STDERR are the same:
            // * Create anonymous pipe to be STDERR for us.
            // * Set STDERR of our process to be WRITE handle to the pipe.
            // * Create a (noninheritable) duplicate of the read handle and
            // * Close the inheritable read handle.
            if (!NativeMethods.CreatePipe(out stdErrRead, out stdErrWrite, ref saAttr, 0))
            {
                Trace.TraceError("Couldn't create the STDERR pipe");
            }
            if (!NativeMethods.SetStdHandle(NativeMethods.StdHandle.ERROR_HANDLE, stdErrWrite))
            {
                Trace.TraceError("Couldn't redirect STDERR!");
            }
            // Create noninheritable read handle and close the inheritable read handle.
            if (!NativeMethods.DuplicateHandle(process.Handle, stdErrRead, process.Handle,
                                               out stdErrReadCopy, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
            {
                Trace.TraceError("Couldn't Duplicate STDERR Handle");
            }
            NativeMethods.CloseHandle(stdErrRead);

            // For the output handles we need a thread to read them
            errorThread = new Thread(ErrorThread);
            errorThread.SetApartmentState(ApartmentState.STA);
            errorThread.Start();

            // The steps for redirecting STDIN:
            // * Create anonymous pipe to be STDIN for us.
            // * Set STDIN of our process to be READ handle to the pipe.
            // * Create a (noninheritable) duplicate of the WRITE handle and
            // * Close the inheritable WRITE handle.

            if (!NativeMethods.CreatePipe(out stdInRead, out stdInWrite, ref saAttr, 0))
            {
                Trace.TraceError("Couldn't create the StdIn pipe");
            }
            if (!NativeMethods.SetStdHandle(NativeMethods.StdHandle.INPUT_HANDLE, stdInRead))
            {
                Trace.TraceError("Couldn't redirect StdIn!");
            }
            // Create noninheritable read handle and close the inheritable read handle.
            if (!NativeMethods.DuplicateHandle(process.Handle, stdInWrite, process.Handle,
                                               out stdInWriteCopy, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS))
            {
                Trace.TraceError("Couldn't Duplicate StdIn Handle");
            }
            NativeMethods.CloseHandle(stdInWrite);
        }

        /// <summary>
        ///     Releases unmanaged resources and performs other cleanup operations
        ///     before the <see cref="Console" /> is reclaimed by garbage collection.
        ///     Use C# destructor syntax for finalization code.
        ///     This destructor will run only if the Dispose method does not get called.
        /// </summary>
        /// <remarks>
        ///     NOTE: Do not provide destructors in types derived from this class.
        /// </remarks>
        ~NativeConsole()
        {
            // Instead of cleaning up in BOTH Dispose() and here ...
            // We call Dispose(false) for the best readability and maintainability.
            Dispose(false);
        }

        #endregion

        #region Methods (5)

        //  Public Methods (2)

        /// <summary>
        ///     Implement IDisposable
        ///     Performs application-defined tasks associated with
        ///     freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        ///     Writes the input.
        /// </summary>
        /// <param name="input">The input.</param>
        /// <param name="args">The args.</param>
        public void WriteInput(string input, params object[] args)
        {
            WriteInput(string.Format(input, args));
        }

        /// <summary>
        ///     Writes the input.
        /// </summary>
        /// <param name="input">The input.</param>
        public void WriteInput(string input)
        {
            byte[] bytes = Encoding.Default.GetBytes(input);
            int written;

            int hresult = 0;
            unsafe
            {
                hresult = NativeMethods.WriteFile(stdInWriteCopy, bytes,
                                                  bytes.Length, out written, null);
            }
            if (hresult != 1)
            {
                throw new Exception("Error Writing to StdIn, HRESULT: " + hresult.ToString());
            }
        }

        //  Private Methods (3)

        /// <summary>
        ///     Handles actual cleanup actions, under two different scenarios
        /// </summary>
        /// <param name="disposing">
        ///     if set to <c>true</c> we've been called directly or
        ///     indirectly by user code and can clean up both managed and unmanaged resources.
        ///     Otherwise it's been called from the destructor/finalizer and we can't
        ///     reference other managed objects (they might already be disposed).
        /// </param>
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!disposed)
            {
                try
                {
                    // // If disposing equals true, dispose all managed resources ALSO.
                    if (disposing)
                    {
                        errorThread.Abort();
                        outputThread.Abort();
                        //WriteInput("\n");

                        byte[] bytes = Encoding.Default.GetBytes("\n" + (char) 26);
                        int written;
                        unsafe
                        {
                            NativeMethods.WriteFile(stdErrWrite, bytes, bytes.Length,
                                                    out written, null);
                            NativeMethods.WriteFile(stdOutWrite, bytes, bytes.Length,
                                                    out written, null);
                        }

                        //errorThread.Join();
                        //outputThread.Join();
                    }

                    // Clean up UnManaged resources
                    // If disposing is false, only the following code is executed.
                    NativeMethods.FreeConsole();
                    //NativeMethods.CloseHandle(stdOutWrite);
                    //NativeMethods.CloseHandle(stdOutReadCopy);
                    //NativeMethods.CloseHandle(stdErrWrite);
                    //NativeMethods.CloseHandle(stdErrReadCopy);
                    NativeMethods.CloseHandle(stdInWriteCopy);
                    NativeMethods.CloseHandle(stdInRead);
                }
                catch (Exception e)
                {
                    Trace.WriteLine(e.Message);
                    Trace.WriteLine(e.StackTrace);
                    throw;
                }
            }
            disposed = true;
        }

        /// <summary>
        ///     The ErrorThread ThreadStart delegate
        /// </summary>
        private unsafe void ErrorThread()
        {
            int BytesRead;
            var BufBytes = new byte[4096];
            // consider wrapping this in a System.IO.FileStream
            try
            {
                while (NativeMethods.ReadFile(stdErrReadCopy, BufBytes, 4096,
                                              out BytesRead, null))
                {
                    if (WriteErrorLine != null)
                    {
                        WriteErrorLine(this, new OutputEventArgs(Encoding.Default.GetString(BufBytes, 0, BytesRead))
                            );
                    }
                }
            }
            catch (ThreadAbortException)
            {
            }
            finally
            {
                NativeMethods.CloseHandle(stdErrWrite);
                NativeMethods.CloseHandle(stdErrReadCopy);
            }
        }

        /// <summary>
        ///     The OutputThread ThreadStart delegate
        /// </summary>
        private unsafe void OutputThread()
        {
            int BytesRead;
            var BufBytes = new byte[4096];
            // consider wrapping this in a System.IO.FileStream
            try
            {
                while (NativeMethods.ReadFile(stdOutReadCopy, BufBytes, 4096,
                                              out BytesRead, null))
                {
                    if (WriteOutputLine != null)
                    {
                        WriteOutputLine(this, new OutputEventArgs(Encoding.Default.GetString(BufBytes, 0, BytesRead))
                            );
                    }
                }
            }
            catch (ThreadAbortException)
            {
            }
            finally
            {
                NativeMethods.CloseHandle(stdOutWrite);
                NativeMethods.CloseHandle(stdOutReadCopy);
            }
        }

        #endregion
    }
}